feat(migrate): os migrate value-shapes —— 引用/结构化 JSON 值形态的按部署门禁(#3438 第 2 项) - #4235
Merged
Conversation
…r reference and structured-JSON value shapes (#3438) ADR-0104 D1's second evidence gate. Media value shapes already enforce once a deployment verifies its file migration (#3681); the reference and structured-JSON classes now get a gate of their own rather than borrowing that one — the file flag attests that file values were migrated and reconciled and says nothing about whether a `lookup` id or a `location` payload is well formed. - spec: `VALUE_SHAPES_MIGRATION_ID` beside its file sibling. - objectql: `scanValueShapes` walks every stored value of the covered classes against the write path's own predicate — `valueShapeViolation` / `isScannableValueShapeField` are exported from record-validator and imported by the scan, so the two cannot drift into disagreeing about "malformed". A test pins that property directly. - objectql: `validateRecord` gains `valueShapeStrict` beside `mediaValueShapeStrict`, and the engine reads the second flag through the same memoized seam. The two flag reads are refactored into one id-parameterised helper so "every way of not knowing answers false" is written once. - cli: `os migrate value-shapes`, dry-run by default. No backfill — a malformed `location` is application data whose correct value only its author knows, so the run reports and prescribes. With nothing to convert, `--apply`'s only write is the flag row, which preserves #3617's invariant trivially. - New escape hatch `OS_ALLOW_LAX_VALUE_SHAPES`, same precedence as its media sibling: opt-out beats all-class opt-in beats the flag. The scanner deliberately does NOT record the flag: readers of a migration flag use the spec contract and only writers depend on platform-objects, so having the engine write one would invert the layering. The CLI composes the two. A truncated or partially-unreadable scan fails the gate even at zero violations — "none in the part we read" is not the claim the flag makes. Verified: objectql 1272, spec 7167, cli 621, service-storage 252 green; all ten spec artifact gates pass (api-surface regenerated for the new exports); ESLint clean on every changed file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
The docs-drift check on the command's own PR flagged content/docs/deployment/cli.mdx, and that one was a real gap rather than package-level noise: the file documents `os migrate files-to-references` in full, so shipping a second gated data migration without an entry would have left it discoverable only by reading the source. Covers the deliberate asymmetry (this run converts nothing — a malformed `location` is application data whose correct value only its author knows), the truncation rule (a partial scan cannot open the gate), and why the flag is separate from the file migration's rather than reusing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv
…efault-flip-vbly5z # Conflicts: # packages/spec/src/system/migration.zod.ts
…otes #4215 landed #3438's third item (a store created from empty attests both flags) while this branch was open, so the merged docs claimed "both flags" without the second one ever being named. - cli.mdx: the file migration's closing paragraph and writing-rules callout had been split off from their own section by the new one inserted above them; move them back so each command's rules sit under that command, and point the "other value classes keep warning" sentence at the gate that now exists for them. - cli.mdx: the created-from-empty section offers an escape hatch per class, so it names both — the media one and the one this branch adds. - v17.mdx: document `os migrate value-shapes` and why its flag is separate, and give the upgrade checklist its own bullet. The old wording told readers OS_DATA_VALUE_SHAPE_STRICT_ENABLED opts in "classes with no migration behind them", which stopped being true here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv
os-zhuang
marked this pull request as ready for review
July 30, 2026 19:20
This was referenced Jul 31, 2026
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
#4243) (#4258) `sys_migration`(#3617 的部署级迁移标记账本)此前由 service-storage 注册,理由写在对象头注释里:「由第一个消费它的服务注册」。那在只有一个消费者时成立,但账本早已有了与存储无关的消费者——#4235 的 value-shapes 门禁扫的是 lookup/location 值形态,整条路径不碰文件;#4215 的从空创建记账同理。代价是 `data-migration-plugins.ts` 里那个绕过:一个和文件毫无关系的迁移,也得把 StorageServicePlugin 启起来,只为让内核里有这张表。 注册点改为 `PlatformObjectsPlugin`。它通过 manifest 服务注册 `SysMigration`(scope: system),并声明 `optionalDependencies: ['com.objectstack.engine.objectql']`,使两者同在时 init 顺序落在 ObjectQL 的 manifest 服务之后,不在时(纯翻译内核)照常降级。`os serve` 本就自动注入该插件,因此每个被服务的内核都带账本,与是否装了存储无关。 #4215 的从空创建记账随注册一起搬家。它留在 storage 的理由原文是「This service owns the call because it registers sys_migration」——前提搬走,调用跟着走。storage 侧只保留读者(file-references 门禁的 `isDataMigrationVerified`),reap guard 行为不变。 `buildDataMigrationPlugins()` 的耦合随之放松:默认只注册 PlatformObjectsPlugin,账本随之而来;`{ storage: true }` 才追加 settings + storage,且只有 `os migrate files-to-references` 传它,因为只有它真的要对着部署的真实存储树做对账。`os migrate value-shapes` 从此不再为一张表启动整个存储服务——该命令严格只读,`--apply` 只写标记本身。 唯一语义变化面:手工组装 StorageServicePlugin 而不组 PlatformObjectsPlugin 的嵌入式内核不再获得账本。方向安全(fail toward retention),changeset 已写明迁移句;cloud 的宿主内核走 serve 装配,不受影响。 Closes #4243 Closes #4236
os-zhuang
pushed a commit
that referenced
this pull request
Jul 31, 2026
The branch was recreated after #4235 merged and neither the PR's opened nor its reopened event produced a workflow run, while other branches pushed in the same window did. This empty commit fires `synchronize`; it collapses away on squash merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv
os-zhuang
pushed a commit
that referenced
this pull request
Jul 31, 2026
…false (#3438) The boot line that names an open value-shape gate (#4253) fired on every deployment there is, and stated something untrue on any deployment that had already settled the posture with an environment switch. Both are one failure: an advisory that speaks where it does not apply is exactly how readers learn to ignore it, and the line's whole value is that a warn-first deployment is told once, credibly, which command ends warn mode. Neither defect is reachable from the suite that shipped with the announcement. `engine.test.ts` mocks `./registry`, and a mocked registry hands the engine exactly the fields the test wrote — so these cases get a sibling file built on the genuine `registerApp → registry` path, which is the only way a test sees what a deployment sees. `objectHasCoveredValueField` — the dormancy short-circuit meant to spare an object with no covered field the flag query — tested raw type membership, while the real registry INJECTS covered-type fields into every object it registers: `organization_id` and `owner_id` (both `system`), `created_by` and `updated_by` (both in `SKIP_FIELDS`), four `lookup`s. `validateRecord` skips every one of them before it reaches the value-shape check, so the short-circuit answered `true` for literally every object, never fired, and its WeakMap memoized a constant. It now counts by the validator's own `isScannableValueShapeField` — the predicate the scanner already imports. Three readings of "a covered field" drifting by one clause is how a gate ends up governing fields nothing enforces, which is the shape #4235 argued against. The announcement also consulted no environment switch, though both postures it reports on short-circuit ahead of the deployment flag it reads. Under `OS_DATA_VALUE_SHAPE_STRICT_ENABLED` enforcement is already on for both classes, so "checked but NOT enforced here" was false; under either opt-out the operator chose leniency deliberately, so naming a migration that cannot change what they get is noise. Each gate consults its own pair — `mediaPostureSetByEnv` and `valueShapePostureSetByEnv`, siblings for the reason `mediaStrictEffective` and `valueShapeStrictEffective` are siblings, since the opt-outs are per-class while the opt-in opens both. Cheapest test first, so a kernel with nothing to say still reaches no flag query. Enforcement is unchanged: same gates, same flags, same default. The flag read was already memoized per process, so what this corrects is the property ADR-0104 states, not a user-visible cost. Verified: objectql 1345 (7 new), cli 628, spec check:generated 8/8, objectql build, check:release-notes and check:doc-authoring pass, ESLint clean. One pre-existing failure on main is untouched by this branch and reproduces with zero of its source changes: `query-expression-conformance.test.ts` > "the removed nested-select object form is refused BY NAME", from #4268. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv
os-zhuang
pushed a commit
that referenced
this pull request
Jul 31, 2026
…false (#3438) The boot line that names an open value-shape gate (#4253) fired on every deployment there is, and stated something untrue on any deployment that had already settled the posture with an environment switch. Both are one failure: an advisory that speaks where it does not apply is exactly how readers learn to ignore it, and the line's whole value is that a warn-first deployment is told once, credibly, which command ends warn mode. Neither defect is reachable from the suite that shipped with the announcement. `engine.test.ts` mocks `./registry`, and a mocked registry hands the engine exactly the fields the test wrote — so these cases get a sibling file built on the genuine `registerApp → registry` path, which is the only way a test sees what a deployment sees. `objectHasCoveredValueField` — the dormancy short-circuit meant to spare an object with no covered field the flag query — tested raw type membership, while the real registry INJECTS covered-type fields into every object it registers: `organization_id` and `owner_id` (both `system`), `created_by` and `updated_by` (both in `SKIP_FIELDS`), four `lookup`s. `validateRecord` skips every one of them before it reaches the value-shape check, so the short-circuit answered `true` for literally every object, never fired, and its WeakMap memoized a constant. It now counts by the validator's own `isScannableValueShapeField` — the predicate the scanner already imports. Three readings of "a covered field" drifting by one clause is how a gate ends up governing fields nothing enforces, which is the shape #4235 argued against. The announcement also consulted no environment switch, though both postures it reports on short-circuit ahead of the deployment flag it reads. Under `OS_DATA_VALUE_SHAPE_STRICT_ENABLED` enforcement is already on for both classes, so "checked but NOT enforced here" was false; under either opt-out the operator chose leniency deliberately, so naming a migration that cannot change what they get is noise. Each gate consults its own pair — `mediaPostureSetByEnv` and `valueShapePostureSetByEnv`, siblings for the reason `mediaStrictEffective` and `valueShapeStrictEffective` are siblings, since the opt-outs are per-class while the opt-in opens both. Cheapest test first, so a kernel with nothing to say still reaches no flag query. Enforcement is unchanged: same gates, same flags, same default. The flag read was already memoized per process, so what this corrects is the property ADR-0104 states, not a user-visible cost. The predicate fix is pinned by reverting it: with raw-type membership restored, `stays silent when no object declares a covered field` fails, because a `note` declaring only text still carries the four injected lookups. Verified on a full workspace build: objectql 1345 (7 new), cli 628, spec check:generated 8/8, check:release-notes and check:doc-authoring pass, ESLint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…false (#3438) (#4284) The boot line that names an open value-shape gate (#4253) fired on every deployment there is, and stated something untrue on any deployment that had already settled the posture with an environment switch. Both are one failure: an advisory that speaks where it does not apply is exactly how readers learn to ignore it, and the line's whole value is that a warn-first deployment is told once, credibly, which command ends warn mode. Neither defect is reachable from the suite that shipped with the announcement. `engine.test.ts` mocks `./registry`, and a mocked registry hands the engine exactly the fields the test wrote — so these cases get a sibling file built on the genuine `registerApp → registry` path, which is the only way a test sees what a deployment sees. `objectHasCoveredValueField` — the dormancy short-circuit meant to spare an object with no covered field the flag query — tested raw type membership, while the real registry INJECTS covered-type fields into every object it registers: `organization_id` and `owner_id` (both `system`), `created_by` and `updated_by` (both in `SKIP_FIELDS`), four `lookup`s. `validateRecord` skips every one of them before it reaches the value-shape check, so the short-circuit answered `true` for literally every object, never fired, and its WeakMap memoized a constant. It now counts by the validator's own `isScannableValueShapeField` — the predicate the scanner already imports. Three readings of "a covered field" drifting by one clause is how a gate ends up governing fields nothing enforces, which is the shape #4235 argued against. The announcement also consulted no environment switch, though both postures it reports on short-circuit ahead of the deployment flag it reads. Under `OS_DATA_VALUE_SHAPE_STRICT_ENABLED` enforcement is already on for both classes, so "checked but NOT enforced here" was false; under either opt-out the operator chose leniency deliberately, so naming a migration that cannot change what they get is noise. Each gate consults its own pair — `mediaPostureSetByEnv` and `valueShapePostureSetByEnv`, siblings for the reason `mediaStrictEffective` and `valueShapeStrictEffective` are siblings, since the opt-outs are per-class while the opt-in opens both. Cheapest test first, so a kernel with nothing to say still reaches no flag query. Enforcement is unchanged: same gates, same flags, same default. The flag read was already memoized per process, so what this corrects is the property ADR-0104 states, not a user-visible cost. The predicate fix is pinned by reverting it: with raw-type membership restored, `stays silent when no object declares a covered field` fails, because a `note` declaring only text still carries the four injected lookups. Verified on a full workspace build: objectql 1345 (7 new), cli 628, spec check:generated 8/8, check:release-notes and check:doc-authoring pass, ESLint clean. Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ADR-0104 D1 的第二个证据门禁(#3438 第 2 项)
媒体类已由 #3681 按部署翻转;本 PR 给引用(
lookup/master_detail/user/tree)与结构化 JSON(location/address/composite/repeater/record/vector)建立它们自己的门禁,而不是去借文件迁移那个标记——后者断言的是「文件值已迁移、归属已对账」,对lookupid 或location载荷是否规范什么都没说。刻意没有回填
姊妹迁移会转换遗留文件值,因为是平台收窄了那个存储形态、因此欠这笔转换。而畸形的
location是应用数据,正确值只有作者知道。所以本命令只报告并给出处方(点名对象、字段、类型、条数、样本 record id、parse issue),由运维修完重跑。由此得到一个漂亮的简化:没有可转换的东西,
--apply唯一的写入就是标记行,于是 #3617 的不变量平凡成立——dry run 什么都不改,且「这次运行是否改变了本部署的姿态」从不取决于它发现了什么。反漂移:扫描器与校验器共用同一个谓词
valueShapeViolation/isScannableValueShapeField从record-validator导出、被扫描器 import。两份「什么算畸形」的实现只要差一个子句,部署就会通过扫描却仍被拒写——那等于标记在断言一个校验器不认的事实,正是 ADR 上一层禁止的「借来的证据」。有一个测试直接钉住这条属性:同一个值必须既被扫描器计数、又被 strict 写入路径拒绝。分层:扫描器不写标记
scanValueShapes是只读的(它接受的 engine 类型根本没有写入面)。标记的读者用 spec 契约,只有写者依赖 platform-objects——这是sys-migration.object.ts自己写下的设计意图。让引擎去写标记会反转依赖方向,所以组合放在 CLI 命令里。其他
OS_ALLOW_LAX_VALUE_SHAPES逃生阀,优先级与媒体半边一致:opt-out > 全类 opt-in > 标记。矛盾配置落在安全侧(错误地宽松只是没人看的告警,错误地严格是能工作的应用停止写入)。与 #4215 的相遇(合并 main 后)
本 PR 开着的时候,#4215 把 #3438 第 3 项(新库从空创建时记账两个标记)落到了 main 上,并顺带在
migration.zod.ts里声明了VALUE_SHAPES_MIGRATION_ID——两边同名常量,这是本次唯一的冲突,解法是留一份声明加 main 的CREATION_ATTESTED_MIGRATION_IDS。已核对 #4215 对engine.ts的改动(getSchemaSyncStats聚合)在合并后完整保留。合并暴露出文档三处不自洽,一并修掉(见
13606e0):lookup/location保持 warn-first」现在有了自己的门禁,改为指向它。OS_DATA_VALUE_SHAPE_STRICT_ENABLED会打开一些背后没有迁移的值类」也不再成立。补上命令、分开标记的理由,以及自己的升级清单条目。OS_ALLOW_LAX_MEDIA_VALUES,补上本 PR 引入的OS_ALLOW_LAX_VALUE_SHAPES。验证(合并 main 之后重跑)
@objectstack/objectql@objectstack/spec@objectstack/cli@objectstack/service-storage@objectstack/platform-objects@objectstack/driver-memoryspec check:generated(8 项生成物)check-release-notes/check-doc-authoring全量构建 71 包通过。(objectql +5 / spec −4 相对合并前,来自 #4215 新增测试与 #4233 退休 plugin 生命周期钩子。)
一个顺带发现(另开 issue,未在本 PR 扩大范围)
sys_migration目前只由 service-storage 注册——这在只有存储一个消费者时说得通,现在有了第二个与存储无关的消费者。实际部署路径没问题(os serve会自动装配 storage),但这层耦合应当解开:账本是平台基础设施,不该由一个可选服务持有。已在data-migration-plugins.ts注释中记录。关联
#3438 三项:第 1 项已合并(
698cbc2),第 3 项由 #4215 合并,本 PR 是第 2 项——合入后 #3438 全部完成。🤖 Generated with Claude Code
https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv